﻿#include "upluginmanager.h"
#include "uiplugin.h"
#include "uplugininfo.h"

#include <QCoreApplication>
#include <QEventLoop>
#include <QDir>
#include <QFile>
#include <QLibrary>
#include <QLibraryInfo>
#include <QMetaProperty>
#include <QDebug>

class UPluginManagerPrivate
{
    Q_DECLARE_PUBLIC(UPluginManager)
public:
    UPluginManagerPrivate(UPluginManager* parent) : q_ptr(parent)
    {
    }
    ~UPluginManagerPrivate() {}

    void loadPlugins();

    void shutdown();

    void setPluginPaths(const QStringList &paths);

    void addObject(QObject* obj);

    void removeObject(QObject* obj);

    void readPluginPaths();

    UPluginManager* q_ptr = nullptr;
private:
    QStringList pluginPaths;
    QList<QObject*> allObjects;
    QList<UPluginInfo*> allPluginInfos;
    QReadWriteLock readWriteLock;
    bool isInitializationDone = false;
};

void UPluginManagerPrivate::addObject(QObject* obj)
{
    Q_Q(UPluginManager);
    QWriteLocker lock(&readWriteLock);
    if (obj == nullptr)
    {
        qWarning() << "PluginManagerPrivate::addObject(): trying to add null object";
        return;
    }
    if (allObjects.contains(obj))
    {
        qWarning() << "PluginManagerPrivate::addObject(): trying to add duplicate object";
        return;
    }

    allObjects.append(obj);
    emit q->objectAdded(obj);
}

void UPluginManagerPrivate::removeObject(QObject* obj)
{
    Q_Q(UPluginManager);
    if (obj == nullptr)
    {
        qWarning() << "PluginManagerPrivate::removeObject(): trying to remove null object";
        return;
    }

    if (!allObjects.contains(obj))
    {
        qWarning() << "PluginManagerPrivate::removeObject(): object not in list:"
                   << obj << obj->objectName();
        return;
    }

    emit q->aboutToRemoveObject(obj);
    QWriteLocker lock(&readWriteLock);
    allObjects.removeAll(obj);
}

void UPluginManagerPrivate::loadPlugins()
{
    Q_Q(UPluginManager);

    for (const auto &plugin : allPluginInfos)
    {
        if (!plugin->loadLibrary())
        {
            allPluginInfos.removeOne(plugin);
            plugin->deleteLater();
            continue;
        }
        if (plugin->iPlugin())
        {
            plugin->iPlugin()->initializationFinished();
        }
    }

    emit q->pluginsChanged();
}

void UPluginManagerPrivate::shutdown()
{
    for (const auto &plugin : allPluginInfos)
    {
        if (plugin)
        {
            plugin->unload();
        }
    }
    qDeleteAll(allPluginInfos);
}

void UPluginManagerPrivate::setPluginPaths(const QStringList &paths)
{
    pluginPaths = paths;
    readPluginPaths();
}

QStringList absolutePaths(const QFileInfoList &files)
{
    QStringList absoluteFilePaths;
    for (const auto &fileInfo : files)
    {
        absoluteFilePaths.append(fileInfo.absoluteFilePath());
    }

    return absoluteFilePaths;
}

QStringList filterLibrary(const QStringList &paths)
{
    QStringList pluginFiles;
    for (const auto &path : paths)
    {
        if (QLibrary::isLibrary(path))
        {
            pluginFiles.append(path);
        }
    }

    return pluginFiles;
}

QStringList pluginFiles(const QStringList &pluginPaths)
{
    QStringList pluginFiles;
    QStringList searchPaths = pluginPaths;
    while (!searchPaths.isEmpty())
    {
        const QDir dir(searchPaths.takeFirst());
        const QFileInfoList files = dir.entryInfoList(QDir::Files | QDir::NoSymLinks);
        const QStringList absoluteFilePaths = absolutePaths(files);
        pluginFiles += filterLibrary(absoluteFilePaths);
    }
    return pluginFiles;
}

void UPluginManagerPrivate::readPluginPaths()
{
    Q_Q(UPluginManager);
    foreach (const QString &pluginFile, pluginFiles(pluginPaths))
    {
        UPluginInfo* spec = new UPluginInfo;
        if (!spec->read(pluginFile))
        {
            delete spec;
            continue;
        }
        allPluginInfos.append(spec);
    }

    emit q->pluginsChanged();
}

UPluginManager* UPluginManager::instance()
{
    static UPluginManager pInstance;
    return &pInstance;
}


UPluginManager::UPluginManager(QObject* parent) : QObject(parent)
    , d_ptr(new UPluginManagerPrivate(this))
{
    Q_D(UPluginManager);

    d->pluginPaths.clear();
    d->allPluginInfos.clear();
    d->allObjects.clear();
}

UPluginManager::~UPluginManager()
{
    shutdown();
}

void UPluginManager::addObject(QObject* obj)
{
    Q_D(UPluginManager);
    d->allObjects.append(obj);
}

void UPluginManager::removeObject(QObject* obj)
{
    Q_D(UPluginManager);
    d->allObjects.removeOne(obj);
}

QList<QObject*> UPluginManager::allObjects()
{
    Q_D(UPluginManager);
    return d->allObjects;
}

QList<UPluginInfo*> UPluginManager::allPlugins()
{
    Q_D(UPluginManager);
    return d->allPluginInfos;
}

QReadWriteLock* UPluginManager::listLock()
{
    Q_D(UPluginManager);
    return &d->readWriteLock;
}

void UPluginManager::loadPlugins()
{
    Q_D(UPluginManager);
    return d->loadPlugins();
}

bool UPluginManager::hasError()
{
    Q_D(UPluginManager);
    foreach (const auto &obj, d->allPluginInfos)
    {
        if (obj->isError())
        {
            return true;
        }
    }
    return false;
}

void UPluginManager::shutdown()
{
    Q_D(UPluginManager);
    d->shutdown();
}

QStringList UPluginManager::pluginPaths()
{
    Q_D(UPluginManager);
    return d->pluginPaths;
}

void UPluginManager::setPluginPaths(const QStringList &paths)
{
    Q_D(UPluginManager);
    d->setPluginPaths(paths);
}

bool UPluginManager::isInitializationDone()
{
    Q_D(UPluginManager);
    return d->isInitializationDone;
}

QObject* UPluginManager::getObjectByName(const QString &name)
{
    Q_D(UPluginManager);
    QReadLocker lock(&d->readWriteLock);
    foreach (QObject* obj, d->allObjects)
    {
        if (obj->objectName() == name)
        {
            return obj;
        }
    }

    return nullptr;
}

QObject* UPluginManager::getObjectByClassName(const QString &className)
{
    Q_D(UPluginManager);
    const QByteArray ba = className.toUtf8();
    QReadLocker lock(&d->readWriteLock);
    foreach (QObject* obj, d->allObjects)
    {
        if (obj->metaObject()->className() == className)
        {
            return obj;
        }
    }

    return nullptr;
}


